import { CustomNotification } from "./notification";
import { Com } from "./com";
import { IframeCleaner } from "./utils/iframeCleaner";
import { RedirectStopper } from "./utils/redirectStopper";
import { urlAccessible } from "./utils/urlAccessible";
import { LogCleaner } from "./utils/logCleaner";

class Inject {
  com: null | Com = null; // communicate with main script
  cn: CustomNotification | null = null; // notify user
  isTop: boolean = window.top === window.self;
  banList: string[] = [];
  enable: boolean = false;
  //plugin
  iframeCleaner: IframeCleaner = new IframeCleaner();
  redirectStopper: RedirectStopper | null = null;
  logCleaner: LogCleaner = new LogCleaner();
  //plugin
  removeLogs: boolean = false;

  constructor() {
    if (this.isTop) this.self();

    this.common();
  }

  common() {
    this.intercept(window);
    this.iframeMess();
    window.addEventListener("DOMContentLoaded", () => {
      this.com = Com.getInstance("main", "isolated");
      this.com.setReceiver(({ data, type }) => {
        switch (type) {
          case "updateList":
            this.banList = data;
            this.redirectStopper &&
              this.redirectStopper.setBanList(this.banList);
            break;
          case "getConfig":
          case "configChange":
            this.enable = data.enable;
            this.iframeCleaner.setEnable(data.removeIframe && this.enable);
            this.redirectStopper &&
              this.redirectStopper.setEnable(data.stopRedirect && this.enable);
            this.logCleaner.setEnable(data.removeLogs && this.enable);
            break;
        }
      });

      this.com.dispatch({
        type: "getConfig",
        data: null,
      });
    });
  }

  self() {
    this.cn = new CustomNotification();
    window.addEventListener("message", ({ data }) => {
      switch (data.type) {
        case "notification":
          if (!this.cn) throw Error("cannot get notification instance");
          this.cn.showMes(data.msg);
          break;
      }
    });

    this.redirectStopper = new RedirectStopper();
  }

  notify(msg: string) {
    if (this.isTop) {
      if (this.cn) this.cn.showMes(msg);
      else throw Error("error with notification instance!");
    } else {
      if (!window.top) throw Error("window.top is null");
      window.top.postMessage(
        {
          type: "notification",
          msg,
        },
        "*"
      );
    }
  }

  policy(type: string, srcElement: any, ...args: any) {
    if (!this.enable) return { block: false };
    const href = srcElement?.action || srcElement?.href;
    const block = !urlAccessible(this.banList, href);

    if (block) this.notify(href);
    return {
      block,
    };
  }

  intercept(win: typeof window) {
    var that = this;
    if (Object.hasOwn(win, "intercepted")) {
      return;
    } else {
      win.intercepted = true;
    }
    const { HTMLAnchorElement, HTMLFormElement } = win;
    HTMLAnchorElement.prototype.click = new Proxy(
      HTMLAnchorElement.prototype.click,
      {
        apply(target, self, args) {
          const { block } = that.policy("dynamic.a.click", self);
          return block ? undefined : Reflect.apply(target, self, args);
        },
      }
    );
    HTMLAnchorElement.prototype.dispatchEvent = new Proxy(
      HTMLAnchorElement.prototype.dispatchEvent,
      {
        apply(target, self, args) {
          const ev = args[0];
          const { block } = that.policy("dynamic.a.dispatch", self, ev);
          return block ? false : Reflect.apply(target, self, args);
        },
      }
    );
    HTMLFormElement.prototype.submit = new Proxy(
      HTMLFormElement.prototype.submit,
      {
        apply(target, self, args) {
          const { block } = that.policy("dynamic.form.submit", self);
          return block ? false : Reflect.apply(target, self, args);
        },
      }
    );
    HTMLFormElement.prototype.dispatchEvent = new Proxy(
      HTMLFormElement.prototype.dispatchEvent,
      {
        apply(target, self, args) {
          const { block } = that.policy("dynamic.form.dispatch", self);
          return block ? false : Reflect.apply(target, self, args);
        },
      }
    );

    /* window.open */
    win.open = new Proxy(win.open, {
      apply(target, self, args) {
        const { block } = that.policy(
          "window.open",
          {
            href: args.length ? args[0] : "",
          },
          null,
          {
            args,
          }
        );
        // _console.log(block);
        if (block) {
          const iframe = document.createElement("iframe");
          iframe.style.display = "none";
          document.body.appendChild(iframe);
          //remove iframe
          setTimeout(() => {
            iframe.remove();
          }, 0);
          //remove iframe
          return iframe.contentWindow;
        }
        return Reflect.apply(target, self, args);
      },
    });

    const click = (e: any) => {
      const a = (e.target.closest("[target]") ||
        e.target.closest("a")) as unknown as
        | HTMLAnchorElement
        | HTMLFormElement;

      // _console.log("click event");
      // if this is not a form or anchor element, ignore the click
      if (a && that.policy("element.click", a, e).block) {
        MouseEvent.prototype.preventDefault.apply(e);
        return true;
      }
    };

    // note that document.open maybe called first !!!
    win.document.write = new Proxy(win.document.write, {
      apply(target, self, args) {
        // _console.log("write...");
        if (!that.com) return;
        const before = win.document.documentElement;
        that.com.dispatch({
          type: "reset",
          data: null,
        });
        const r = Reflect.apply(target, self, args);
        if (win.document.documentElement !== before) {
          // _console.log("changed");
          that.com.reset();
          // (win.document.documentElement as any) = self.documentElement;
          // self.removeEventListener("click", click);
          self.addEventListener("click", click, true);
        }
        return r;
      },
    });

    win.document.addEventListener("click", click, true); // with capture;
    win.document.addEventListener("click", click); // without capture;
  }
  iframeMess() {
    const { HTMLIFrameElement, HTMLFrameElement } = window;
    const that = this;

    const wf = Object.getOwnPropertyDescriptor(
      HTMLFrameElement.prototype,
      "contentWindow"
    );
    Object.defineProperty(HTMLFrameElement.prototype, "contentWindow", {
      configurable: true,
      enumerable: true,
      get: function () {
        const w = wf?.get?.call(this);
        try {
          that.intercept(w);
        } catch (e) {}
        return w;
      },
    });
    const wif = Object.getOwnPropertyDescriptor(
      HTMLIFrameElement.prototype,
      "contentWindow"
    );
    Object.defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
      configurable: true,
      enumerable: true,
      get: function () {
        const w = wif?.get?.call(this);
        try {
          that.intercept(w);
          // w.open = () => {
          //   _console.log("you open window");
          // };
        } catch (e) {}
        return w;
      },
    });
    const cf = Object.getOwnPropertyDescriptor(
      HTMLFrameElement.prototype,
      "contentDocument"
    );
    Object.defineProperty(HTMLFrameElement.prototype, "contentDocument", {
      configurable: true,
      enumerable: true,
      get: function () {
        const d = cf?.get?.call(this);
        try {
          that.intercept(d.defaultView);
        } catch (e) {}
        return d;
      },
    });
    const cif = Object.getOwnPropertyDescriptor(
      HTMLIFrameElement.prototype,
      "contentDocument"
    );
    Object.defineProperty(HTMLIFrameElement.prototype, "contentDocument", {
      configurable: true,
      enumerable: true,
      get: function () {
        const d = cif?.get?.call(this);
        try {
          that.intercept(d.defaultView);
        } catch (e) {}
        return d;
      },
    });
  }
  exist<T>(entity: T | null, success: (en: T) => void) {
    if (!entity) {
      console.trace("entity does not exist!", entity);
      return;
    }
    success(entity);
  }
}

window.inject_main_class = new Inject();
